package grammar;

import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import constraint.Constraint;
import constraint.ConstraintAdjacent;

import edu.uci.ics.jung.graph.DirectedSparseGraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.Pair;
import geometry.Box;
import geometry.ComboBox;
import geometry.Geometry;
import geometry.Sphere;
import graph.GNode;
import graph.NodeType;
import graph.SubGGraph;

/**
 * A design is a simultaneous geometric and graphical representation.
 * 
 * The geometry is captured in the list of geometries, which correspond to 
 * nodes in the GGraph. Each of the geometries cs has the World cs as its parent.  
 * 
 * The correspondence between the nodes in the graph and geometries are maintained
 * by the design.
 * 
 * @author jay
 *
 */
public class Design {
    private Product product;
    private Map<GNode, List<GNode>> productToAssemblies;
    private DirectedSparseGraph<GNode, Constraint> assemblies;
    private Map<GNode, List<GNode>> assemblyToParts;
    private DirectedSparseGraph<GNode, Constraint> partGraph;
    private Map<GNode, List<GNode>> partToVariables;
    private DirectedSparseGraph<GNode, Constraint> varGraph;
	
    public Design() {
        productToAssemblies = new HashMap<GNode, List<GNode>>();
        assemblies = new DirectedSparseGraph<GNode, Constraint>();
        assemblyToParts = new HashMap<GNode, List<GNode>>();
        partGraph = new DirectedSparseGraph<GNode, Constraint>();
        varGraph = new DirectedSparseGraph<GNode, Constraint>();
        partToVariables = new HashMap<GNode, List<GNode>>();
    }
    
    public Design copy() {
        Design design = new Design();
        design.product = (Product) product.copy();
        
        ///////////////////////////////////////ASSEBLY//////////////////////////////////////////////////////////////////////////////
        Map<GNode, GNode> oldAssemToNewAssem = new HashMap<GNode, GNode>();
        
        for (GNode prod : productToAssemblies.keySet()) {
            List<GNode> assems = new ArrayList<GNode>();
            design.productToAssemblies.put(design.product, assems);            
            for (GNode assem : productToAssemblies.get(prod)) {
                GNode assemCopy = assem.copy();
                oldAssemToNewAssem.put(assem, assemCopy);
                assems.add(assemCopy);
            }
        }
        
        for (GNode assemCopy : oldAssemToNewAssem.values()) {
            design.assemblies.addVertex(assemCopy);
        }
        
        Map<Constraint, Constraint> oldEdgeToNewEdgeA = new HashMap<Constraint, Constraint>();
        for (GNode assem : oldAssemToNewAssem.keySet()) {
            Collection<Constraint> edges = this.assemblies.getIncidentEdges(assem);
            for (Constraint edge : edges) {
                Constraint edgeCopy = oldEdgeToNewEdgeA.get(edge);
                if (edgeCopy == null) {
                    edgeCopy = edge.copy();                    
                    oldEdgeToNewEdgeA.put(edge, edgeCopy);
                    Pair<GNode> endpoints = this.assemblies.getEndpoints(edge);
                    GNode new1 = oldAssemToNewAssem.get(endpoints.getFirst());
                    GNode new2 = oldAssemToNewAssem.get(endpoints.getSecond());
                    design.assemblies.addEdge(edgeCopy, new1, new2);
                }                
            }
        }
        //////////////////////////////////////////PART//////////////////////////////////////////////////////////////////////////////////
        Map<GNode, GNode> oldPartToNewPart= new HashMap<GNode, GNode>();
        
        for (GNode assem : this.assemblyToParts.keySet()) {
            List<GNode> newParts = new ArrayList<GNode>();
            GNode newAssem = oldAssemToNewAssem.get(assem);
            design.assemblyToParts.put(newAssem, newParts);            
            
            for (GNode part : this.assemblyToParts.get(assem)) {
                GNode partCopy = part.copy();
                oldPartToNewPart.put(part, partCopy);
                newParts.add(partCopy);
            }
        }
        
        for (GNode partCopy : oldPartToNewPart.values()) {
            design.partGraph.addVertex(partCopy);
        }
        
        Map<Constraint, Constraint> oldEdgeToNewEdgeP = new HashMap<Constraint, Constraint>();
        for (GNode part : oldPartToNewPart.keySet()) {
            Collection<Constraint> edges = this.partGraph.getIncidentEdges(part);
            for (Constraint edge : edges) {
                Constraint edgeCopy = oldEdgeToNewEdgeP.get(edge);
                if (edgeCopy == null) {
                    edgeCopy = edge.copy();                    
                    oldEdgeToNewEdgeP.put(edge, edgeCopy);
                    Pair<GNode> endpoints = this.partGraph.getEndpoints(edge);
                    GNode new1 = oldPartToNewPart.get(endpoints.getFirst());
                    GNode new2 = oldPartToNewPart.get(endpoints.getSecond());
                    design.partGraph.addEdge(edgeCopy, new1, new2);
                }                
            }
        }
        return design;
    }

    public void printGraph() {
        System.out.println(graphToString());
    }
    
    public String graphToString() {
        // TODO Auto-generated method stub
        return "";
    }

    @SuppressWarnings("unchecked")
    public Collection<Geometry> getGeometries() {
        Collection<GNode> nodes = partGraph.getVertices();
        Set<Geometry> allGeoms = new HashSet<Geometry>((Collection<? extends Geometry>) nodes);
        return allGeoms;
    }
    
    /**
     * 
     * @return a single graph that is a compilation of all subgraphs in the GGraph for display purposes.
     */
    public Graph<GNode, Constraint> getComboGraph() {
        Graph<GNode, Constraint> comboGraph = new DirectedSparseGraph<GNode, Constraint>();        
        comboGraph.addVertex(product);
        for (GNode node : this.assemblies.getVertices()) {
            comboGraph.addVertex(node);
        }
        for (Constraint edge : this.assemblies.getEdges()) {
            comboGraph.addEdge(edge, assemblies.getEndpoints(edge).getFirst(), assemblies.getEndpoints(edge).getSecond());
        }
        for (GNode node : this.partGraph.getVertices()) {
            comboGraph.addVertex(node);
        }
        for (Constraint edge : this.partGraph.getEdges()) {
            comboGraph.addEdge(edge, partGraph.getEndpoints(edge).getFirst(), partGraph.getEndpoints(edge).getSecond());
        }
        for (GNode node : this.varGraph.getVertices()) {
            comboGraph.addVertex(node);
        }
        for (Constraint edge : this.varGraph.getEdges()) {
            comboGraph.addEdge(edge, varGraph.getEndpoints(edge).getFirst(), varGraph.getEndpoints(edge).getSecond());
        }
        for (GNode node1 : productToAssemblies.keySet()) {
            for (GNode node2 : productToAssemblies.get(node1)) {
                comboGraph.addEdge(new ConstraintAdjacent(node1, node2), node1, node2);
            }
        }
        for (GNode node1 : this.assemblyToParts.keySet()) {
            for (GNode node2 : assemblyToParts.get(node1)) {
                comboGraph.addEdge(new ConstraintAdjacent(node1, node2), node1, node2);
            }
        }
        for (GNode node1 : this.partToVariables.keySet()) {
            for (GNode node2 : partToVariables.get(node1)) {
                comboGraph.addEdge(new ConstraintAdjacent(node1, node2), node1, node2);
            }
        }
        return comboGraph;
    }
    
    public String toString() {
        String s = "--------GRAPH--------";
        s=s.concat(graphToString());
        s=s.concat("\n--------GEOMETRY--------");
        for (GNode geom : getVertices()) {            
            s=s.concat("\n\t<" + geom.getLabels()+">");
            s=s.concat(geom.toString());
        }
        return s;
    }

//    public void  removeAssociatedGeometries(GNode node) {
//        Geometry geom = nodeToGeomMap.get(node);
//        if (geom == null) {
//            return;
//        }
//        nodeToGeomMap.remove(node); 
//    }
//
//    public void addAssociatedGeometry(GNode node, Geometry geometry) {
//        //add the geometry
//        this.nodeToGeomMap.put(node, geometry);
//    }

    public void writeVRML(String fileName) {
        try {
            PrintWriter out = new PrintWriter(fileName);
            out.print("#VRML V2.0 utf8\n");

            for (GNode node : partGraph.getVertices()) {
                Geometry geometry = (Geometry) node;
                out.print(geometry.toVRML());
            }
            out.close();
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

//    public boolean hasAssociatedGeometry(GNode node) {
//        Geometry geom = this.nodeToGeomMap.get(node);
//        if (geom == null) {
//            return false;
//        }
//        return true;
//    }
//
//    public GNode getTransformationSource(GNode node, GraphMatch match) {
//        return graph.getTransformationSource(node, match);
//    }
    
    public boolean hasSelfIntersectingGeometry() {
        List<Geometry> geometries = new ArrayList<Geometry>();
        for (int i = 0; i<geometries.size(); i++) {
            for (int j = i+1; j < geometries.size(); j++) {
                if (geometries.get(i).intersects(geometries.get(j))) {
                    return true;
                }
            }
        }
        return false;
    }

//    public Geometry removeGeometry(GNode node) {
//        return nodeToGeomMap.remove(node);
//    }
//
//    public Collection<GNode> getSameLevelNeighbors(GNode node) {
//        return graph.getSameLevelNeighbors(node);
//    }
//
//    public Geometry getGeometry(GNode node) {
//        return this.nodeToGeomMap.get(node);
//    }
//
//    public boolean isConstrained(Geometry geometry) {
//        for (Constraint constraint : network.getConstraints()) {
//            constraint.
//        }
//        return false;
//    }

    public Product setProduct(String label) {
        product = new Product(label);
        return product;
    }
    
    public void setProduct(Product product) {
        this.product = product;
    }

    public Assembly addAssembly(String label) {
        Assembly assembly = new Assembly(label);
        List<GNode> assemblies = productToAssemblies.get(product);
        if (assemblies == null) {
            assemblies = new ArrayList<GNode>();
            assemblies.add(assembly);
            productToAssemblies.put((GNode)product, assemblies);
        } else {
            assemblies.add((GNode)assembly);
        }
        this.assemblies.addVertex(assembly);
        return assembly;
    }
    
    public Assembly addAssembly(Assembly assembly) {
        List<GNode> assemblies = productToAssemblies.get(product);
        if (assemblies == null) {
            assemblies = new ArrayList<GNode>();
            assemblies.add(assembly);
            productToAssemblies.put((GNode)product, assemblies);
        } else {
            assemblies.add((GNode)assembly);
        }
        this.assemblies.addVertex(assembly);
        return assembly;
    }

    public Box addBox(Assembly assembly, double xdim, double ydim, double zdim, String label) {
        Box box = new Box(xdim, ydim, zdim, label);
        addGeometry(assembly, box);
        partGraph.addVertex(box);
        return box;
    }
    
    public Box addBox(Assembly[] assemblies, double xdim, double ydim, double zdim, String label) {
        Box box = new Box(xdim, ydim, zdim, label);
        for (int i=0; i< assemblies.length; i++) {
            addGeometry(assemblies[i], box);
        }
        partGraph.addVertex(box);
        return box;
    }

    
    public void addGeometry(Assembly assembly, Geometry geometry) {
        List<GNode> geoms = assemblyToParts.get(assembly);
        if (geoms == null) {
            geoms = new ArrayList<GNode>();
            geoms.add((GNode) geometry);
            assemblyToParts.put((GNode)assembly, geoms);
        } else {
            geoms.add((GNode)geometry);
        }
        partGraph.addVertex((GNode) geometry);
    }

    public ComboBox addComboBox(Assembly assembly, List<Box> boxes, String label) {
        ComboBox combo = new ComboBox(boxes, "COMBO");
        addGeometry(assembly, combo);
        return combo;
    }

    public int getNodeCount() {
        return this.varGraph.getVertexCount() + this.partGraph.getVertexCount() 
        + this.assemblies.getVertexCount() + 1 /*1 for the product*/;
    }

    public int getNodeCount(NodeType type) {
        return getGraphOfType(type).getVertexCount();
    }
    
    public Graph<GNode, Constraint>getGraphOfType(NodeType type) {
        Graph<GNode, Constraint> graph = null;
        
        if (type == NodeType.PRODUCT) {
            graph = null;
        } else if (type == NodeType.ASSEMBLY) {
            graph = this.assemblies;
        } else if (type == NodeType.PART) {
            graph = partGraph;
        } else if (type == NodeType.DETAIL) {
            graph = this.varGraph;
        }
        return graph;
    }
    
    /**
     * 
     * @param type
     * @return the list of subgraphs at the level indicated by type
     */
    public List<SubGGraph> getChildSubgraphs(NodeType type) {
        Map<GNode, List<GNode>> connectGraph = null;
        Graph<GNode, Constraint> graph = null;
        if (type == NodeType.PRODUCT) {
            return null;
        } else if (type == NodeType.ASSEMBLY) {
            connectGraph = this.productToAssemblies;
            graph = this.assemblies;
        } else if (type == NodeType.PART) {
            connectGraph = this.assemblyToParts;
            graph = this.partGraph;
        } else if (type == NodeType.DETAIL) {
            connectGraph = this.partToVariables;
            graph = this.varGraph;
        }
        
        Set<GNode> parents = new HashSet<GNode>(connectGraph.keySet()); 
        List<SubGGraph> subgraphs = new ArrayList<SubGGraph>();
        for (GNode parent : parents) {
            SubGGraph subgraph = new SubGGraph(parent);
            List<GNode> subgraphNodes = new ArrayList<GNode>(connectGraph.get(parent));
            if (subgraphNodes.isEmpty()) {
                continue;
            }
            subgraphs.add(subgraph);
            for (int i=0; i<subgraphNodes.size(); i++) {
                subgraph.addNode(subgraphNodes.get(i));
                for (int j=i+1; j<subgraphNodes.size(); j++) {
                    if (graph.isNeighbor(subgraphNodes.get(i), subgraphNodes.get(j))) {
                        subgraph.addEdge(subgraphNodes.get(i), subgraphNodes.get(j));
                    }
                }
            }
        }
        return subgraphs;
    }

    public boolean hasProduct() {
        if (product != null) {
            return true;
        }
        return false;
    }

    public Product getProduct() {
        return product;
    }

    public Collection<GNode> getVertices() {
        List <GNode> nodes = new ArrayList<GNode>();
        nodes.add(product);
        nodes.addAll(assemblies.getVertices());
        nodes.addAll(partGraph.getVertices());
        nodes.addAll(varGraph.getVertices());
        return nodes;
    }

    public Collection<GNode> getNeighbors(GNode node) {
        List <GNode> neighbors = new ArrayList<GNode>();
        if (node.getType() == NodeType.PRODUCT) {

        } else if (node.getType() == NodeType.ASSEMBLY) {
            neighbors.addAll(assemblies.getNeighbors(node));
        } else if (node.getType() == NodeType.PART) {
            neighbors.addAll(partGraph.getNeighbors(node));
        } else if (node.getType() == NodeType.DETAIL) {
            neighbors.addAll(varGraph.getNeighbors(node));
        }
        return neighbors;
    }

    public boolean containsVertex(GNode node) {
        if (node.getType() == NodeType.PRODUCT && node == product) {
            return true;
        } else if (node.getType() == NodeType.ASSEMBLY) {
            return assemblies.containsVertex(node);
        } else if (node.getType() == NodeType.PART) {
            return partGraph.containsVertex(node);
        } else if (node.getType() == NodeType.DETAIL) {
            return varGraph.containsVertex(node);
        }
        return false;
    }

    public Collection<GNode> getNodes(NodeType type) {
        List <GNode> nodes = new ArrayList<GNode>();
        if (type == NodeType.PRODUCT) {
            nodes.add(product);
        } else if (type == NodeType.ASSEMBLY) {
            nodes.addAll(assemblies.getVertices());
        } else if (type == NodeType.PART) {
            nodes.addAll(partGraph.getVertices());
        } else if (type == NodeType.DETAIL) {
            nodes.addAll(varGraph.getVertices());
        }
        return nodes;
    }

    public void removeNode(GNode node) {
        if (node == product) {
            product = null;
        }
        productToAssemblies.remove(node);
        assemblies.removeVertex(node);
        assemblyToParts.remove(node);
        partGraph.removeVertex(node);
        partToVariables.remove(node);
        varGraph.removeVertex(node);
    }

    public Constraint addEdge(GNode fromNode, GNode toNode) {
        if(fromNode.getType() != toNode.getType()) {
            return addConnectEdge(fromNode, toNode);
        }

        Constraint edge = null;
        if (fromNode.getType() == NodeType.PRODUCT) {
        } else if (fromNode.getType() == NodeType.ASSEMBLY) {
            edge = new ConstraintAdjacent(fromNode, toNode);
            assemblies.addEdge(edge, fromNode, toNode);
        } else if (fromNode.getType() == NodeType.PART) {
            edge = new ConstraintAdjacent(fromNode, toNode);
            partGraph.addEdge(edge, fromNode, toNode);
        } else if (fromNode.getType() == NodeType.DETAIL) {
            edge = new ConstraintAdjacent(fromNode, toNode);
            varGraph.addEdge(edge, fromNode, toNode);
        } 
        return edge;
    }
    
    private Constraint addConnectEdge(GNode fromNode, GNode toNode) {
        Constraint edge = null;
        Map<GNode, List<GNode>> map = null;
        if (fromNode.getType() == NodeType.PRODUCT && toNode.getType() == NodeType.ASSEMBLY) {
            map = this.productToAssemblies;
        } else if (fromNode.getType() == NodeType.ASSEMBLY && toNode.getType() == NodeType.PART) {
            map = this.assemblyToParts;
        } else if (fromNode.getType() == NodeType.PART && toNode.getType() == NodeType.DETAIL) {
            map = this.partToVariables;
        }

        List<GNode> nodes = map.get(fromNode);
        if (nodes == null) {
            nodes = new ArrayList<GNode>();
            nodes.add(toNode);
            map.put(fromNode, nodes);
        } else {
            nodes.add(toNode);
        }
        return edge;
    }

    public Map<GNode, List<GNode>> getConnectMapOfType(NodeType type) {
        Map<GNode, List<GNode>> map = null;
        if (type == NodeType.PRODUCT) {
            map = this.productToAssemblies;
        } else if (type == NodeType.ASSEMBLY) {
            map = this.assemblyToParts;
        } else if (type == NodeType.PART) {
            map = this.partToVariables;
        } 
        return map;
    }

    public void addNode(GNode node) {
        if (node.getType() == NodeType.PRODUCT) {
            product = (Product) node;
        } else if (node.getType() == NodeType.ASSEMBLY) {
            assemblies.addVertex(node);
        } else if (node.getType() == NodeType.PART) {
            partGraph.addVertex(node);
        } else if (node.getType() == NodeType.DETAIL) {
            varGraph.addVertex(node);
        }  
    }

    public GNode getParent(GNode node) {
        Map<GNode, List<GNode>> map = null;
        if (node.getType() == NodeType.ASSEMBLY) {
            map = this.productToAssemblies;
        } else if (node.getType() == NodeType.PART) {
            map = this.assemblyToParts;
        } else if (node.getType() == NodeType.DETAIL) {
            map = this.partToVariables;
        }
        for (GNode parent : map.keySet()) {
            for (GNode child: map.get(parent)) {
                if (node == child) {
                    return parent;                    
                }
            }
        }
        return null;
    }

    public Sphere addSphere(Assembly assembly, double radius, String label) {
        Sphere sphere = new Sphere(radius, label);
        addGeometry(assembly, sphere);
        partGraph.addVertex(sphere);
        return sphere;
    }
}
